"""
基于PyAudio的音频播放器实现
"""
import os
import time
import threading
import numpy as np
import pyaudio
import tempfile
import logging
from typing import Optional, Callable, Dict, Any, List
from enum import Enum
from PyQt5.QtCore import QObject, pyqtSignal
from scipy.io import wavfile
import platform


class PlayerState(Enum):
    """播放器状态枚举"""
    STOPPED = "STOPPED"
    PLAYING = "PLAYING"
    PAUSED = "PAUSED"


class PyAudioPlayer(QObject):
    """
    PyAudio播放器封装类
    提供音频播放和设备选择功能
    """
    # 定义信号
    state_changed = pyqtSignal(str)
    position_changed = pyqtSignal(float)
    playback_completed = pyqtSignal()
    playback_error = pyqtSignal(str)
    
    def __init__(self, logger=None):
        """
        初始化播放器
        
        @param logger: 可选的日志记录器
        """
        super().__init__()
        
        # 设置日志记录器
        self.logger = logger or logging.getLogger(__name__)
        
        # 初始化PyAudio
        self.pa = pyaudio.PyAudio()
        
        # 设备信息
        self._current_device = None  # 当前设备名称
        self._device_info = None     # 当前设备详细信息
        self._devices = None         # 所有设备列表
        
        # 播放状态
        self._state = PlayerState.STOPPED
        self._stream = None
        self._volume = 1.0           # 音量 (0.0-1.0)
        
        # 线程和锁
        self._play_thread = None
        self._lock = threading.Lock()
        self._should_stop = threading.Event()
        self._paused = threading.Event()
        
        # 文件信息
        self._current_file = None
        self._audio_data = None      # 音频数据（NumPy数组）
        self._position = 0
        self._duration = 0
        self._sample_rate = 0
        self._channels = 0
        self._frame_position = 0
        self._frames = 0
        
        # 临时文件
        self._temp_file = None
        
        # 回调函数
        self._completion_callback = None
        self._error_callback = None
        
        # 缓存所有设备信息
        self._cache_all_devices()
        
        self.logger.info(f"PyAudio播放器初始化完成，发现 {len(self.get_output_devices())} 个输出设备")
        
    def _cache_all_devices(self):
        """缓存所有设备信息"""
        self._devices = []
        
        # 首先添加默认设备
        self._devices.append({"name": "默认设备", "index": -1, "default": True})
        
        # 获取所有设备信息
        device_count = self.pa.get_device_count()
        for i in range(device_count):
            try:
                device_info = self.pa.get_device_info_by_index(i)
                
                # 只添加输出设备
                if device_info['maxOutputChannels'] > 0:
                    is_default = self.pa.get_default_output_device_info()['index'] == i
                    
                    self._devices.append({
                        "name": device_info['name'],
                        "index": i,
                        "channels": device_info['maxOutputChannels'],
                        "sample_rate": int(device_info['defaultSampleRate']),
                        "default": is_default
                    })
                    
                    self.logger.debug(f"发现输出设备: {device_info['name']}, 索引: {i}, "
                                    f"通道数: {device_info['maxOutputChannels']}, "
                                    f"采样率: {int(device_info['defaultSampleRate'])}, "
                                    f"默认: {is_default}")
            except Exception as e:
                self.logger.warning(f"获取设备 {i} 信息时出错: {str(e)}")

    def get_output_devices(self) -> List[Dict[str, Any]]:
        """
        获取所有音频输出设备
        
        @return: 输出设备列表
        """
        if not self._devices:
            self._cache_all_devices()
        return self._devices

    def set_output_device(self, device_name: str) -> bool:
        """
        设置输出设备
        
        @param device_name: 设备名称
        @return: 是否成功设置
        """
        # 如果是默认设备，直接设置
        if device_name == "默认设备":
            self._current_device = "默认设备"
            self._device_info = None
            self.logger.info("已设置为默认输出设备")
            return True
        
        # 查找完全匹配的设备
        for device in self.get_output_devices():
            if device["name"] == device_name:
                self._current_device = device_name
                self._device_info = device
                self.logger.info(f"已设置输出设备: {device_name}, 索引: {device['index']}")
                return True
        
        # 如果没有找到完全匹配的设备，尝试部分匹配
        best_match = None
        best_score = 0
        for device in self.get_output_devices():
            # 如果设备名称包含提供的名称，或提供的名称包含设备名称的一部分
            if device_name in device["name"] or device["name"] in device_name:
                # 计算匹配度，使用公共字符数作为简单度量
                common_chars = sum(1 for c in device["name"] if c in device_name)
                if common_chars > best_score:
                    best_score = common_chars
                    best_match = device
        
        # 如果找到部分匹配
        if best_match:
            self._current_device = best_match["name"]
            self._device_info = best_match
            self.logger.info(f"已设置输出设备(部分匹配): {best_match['name']}, 索引: {best_match['index']}")
            return True
            
        # 如果没有找到匹配的设备，使用默认设备
        self.logger.warning(f"未找到设备: {device_name}, 将使用默认设备")
        self._current_device = "默认设备"
        self._device_info = None
        return False

    def get_current_device(self) -> str:
        """
        获取当前输出设备名称
        
        @return: 当前设备名称
        """
        return self._current_device or "默认设备"

    def set_volume(self, volume: float) -> None:
        """
        设置音量
        
        @param volume: 音量值 (0.0-1.0)
        """
        # 确保音量在有效范围内
        self._volume = max(0.0, min(1.0, volume))
        self.logger.debug(f"设置音量: {self._volume}")

    def get_volume(self) -> float:
        """
        获取当前音量
        
        @return: 当前音量 (0.0-1.0)
        """
        return self._volume

    def set_completion_callback(self, callback: Callable[[], None]) -> None:
        """
        设置播放完成回调函数
        
        @param callback: 回调函数
        """
        # 首先断开之前的连接
        try:
            self.playback_completed.disconnect()
        except:
            pass
        
        # 连接新的回调
        if callback:
            self.playback_completed.connect(callback)

    def set_error_callback(self, callback: Callable[[str], None]) -> None:
        """
        设置错误回调函数
        
        @param callback: 回调函数，接收错误信息字符串参数
        """
        # 首先断开之前的连接
        try:
            self.playback_error.disconnect()
        except:
            pass
        
        # 连接新的回调
        if callback:
            self.playback_error.connect(callback)

    def _audio_callback(self, in_data, frame_count, time_info, status):
        """
        音频回调函数
        
        @param in_data: 输入数据 (unused)
        @param frame_count: 请求的帧数
        @param time_info: 时间信息
        @param status: 状态标志
        @return: (data, flag)
        """
        # 如果暂停，返回静音数据
        if self._paused.is_set():
            # 返回静音数据(全0)，继续标志
            return (b'\x00' * frame_count * self._channels * 2, pyaudio.paContinue)
        
        # 如果应该停止，返回空数据和完成标志
        if self._should_stop.is_set():
            return (None, pyaudio.paComplete)
        
        # 读取音频数据
        if self._audio_data is not None:
            # 计算当前位置和要读取的帧数
            start_pos = self._frame_position
            end_pos = min(start_pos + frame_count, self._frames)
            frames_to_read = end_pos - start_pos
            
            # 如果没有更多数据，返回完成标志
            if frames_to_read <= 0:
                return (None, pyaudio.paComplete)
            
            # 获取音频数据片段
            if len(self._audio_data.shape) > 1:  # 多声道
                data_chunk = self._audio_data[start_pos:end_pos, :].copy()
            else:  # 单声道
                data_chunk = self._audio_data[start_pos:end_pos].copy()
            
            # 更新当前位置
            self._frame_position += frames_to_read
            
            # 发送位置更新信号
            self.position_changed.emit(self._frame_position / self._sample_rate)
            
            # 如果音量不是1.0，则调整音量
            if self._volume != 1.0:
                try:
                    # 改进的音量调整逻辑
                    # 1. 先转换为float32类型进行处理，防止整数溢出
                    data_float = data_chunk.astype(np.float32)
                    
                    # 2. 应用音量调整
                    data_float = data_float * self._volume
                    
                    # 3. 软限幅处理，防止爆音
                    # 计算最大可能值
                    max_possible = np.iinfo(np.int16).max
                    # 实现软限幅：在接近最大值时逐渐压缩信号
                    threshold = 0.8 * max_possible  # 设置阈值为最大值的80%
                    if np.any(np.abs(data_float) > threshold):
                        self.logger.debug("检测到信号接近最大值，应用软限幅")
                        # 找出超过阈值的部分
                        mask = np.abs(data_float) > threshold
                        # 计算压缩后的值：threshold + (value - threshold) * 0.5
                        scaling = 0.5  # 压缩系数
                        compressed = np.sign(data_float[mask]) * (threshold + (np.abs(data_float[mask]) - threshold) * scaling)
                        # 应用压缩
                        data_float[mask] = compressed
                    
                    # 4. 硬限幅，确保不超过int16范围
                    data_float = np.clip(data_float, -max_possible, max_possible)
                    
                    # 5. 转回int16
                    data_chunk = data_float.astype(np.int16)
                    
                except Exception as e:
                    self.logger.warning(f"音量调整失败: {str(e)}")
            
            # 转为字节数据
            return (data_chunk.tobytes(), pyaudio.paContinue)
        else:
            # 没有音频数据，返回完成标志
            return (None, pyaudio.paComplete)

    def _play_thread_func(self):
        """
        播放线程函数
        """
        try:
            # 重置停止和暂停标志
            self._should_stop.clear()
            self._paused.clear()
            
            # 设置状态为播放中
            self._set_state(PlayerState.PLAYING)
            
            # 开始播放流
            self._stream.start_stream()
            
            # 等待流完成或停止信号
            while self._stream.is_active() and not self._should_stop.is_set():
                time.sleep(0.1)
                # 如果暂停标志被设置，但状态不是暂停，更新状态
                if self._paused.is_set() and self._state != PlayerState.PAUSED:
                    self._set_state(PlayerState.PAUSED)
                    self.logger.debug("播放线程检测到暂停标志，已更新状态为暂停")
            
            # 如果没有收到停止信号，说明播放完成
            if not self._should_stop.is_set() and not self._paused.is_set():
                self.logger.info("音频播放完成")
                
                # 发送完成信号
                self.playback_completed.emit()
            else:
                if self._should_stop.is_set():
                    self.logger.info("播放被主动停止")
                elif self._paused.is_set():
                    self.logger.info("播放被暂停")
            
            # 关闭流
            self._cleanup_stream()
            
            # 如果不是因为暂停而停止，则设置状态为停止
            if not self._paused.is_set():
                self._set_state(PlayerState.STOPPED)
                
        except Exception as e:
            self.logger.error(f"播放线程异常: {str(e)}")
            import traceback
            self.logger.error(traceback.format_exc())
            
            # 发送错误信号
            self.playback_error.emit(str(e))
            
            self._cleanup_stream()
            self._set_state(PlayerState.STOPPED)

    def _cleanup_stream(self):
        """清理流和文件"""
        if self._stream:
            try:
                self._stream.stop_stream()
                self._stream.close()
                self.logger.info("音频流已关闭")
            except Exception as e:
                self.logger.error(f"关闭音频流时出错: {str(e)}")
            self._stream = None
        
        # 只有在非暂停状态下才清理音频数据和临时文件
        if not self._paused.is_set():
            self.logger.info("清理音频数据和临时文件")
            # 清理音频数据和临时文件
            self._audio_data = None
            self._clean_temp_file()
        else:
            self.logger.info("暂停状态下保留音频数据，不清理临时文件")

    def _clean_temp_file(self):
        """清理临时文件"""
        if self._temp_file and os.path.exists(self._temp_file):
            try:
                os.remove(self._temp_file)
                self.logger.debug(f"已删除临时文件: {self._temp_file}")
                self._temp_file = None
            except Exception as e:
                self.logger.warning(f"删除临时文件失败: {self._temp_file}, 错误: {str(e)}")

    def play(self, file_path: str) -> bool:
        """
        播放音频文件
        
        @param file_path: 音频文件路径
        @return: 是否成功播放
        """
        try:
            # 停止当前播放
            self.stop()
            
            # 检查文件是否存在
            if not os.path.exists(file_path):
                self.logger.error(f"文件不存在: {file_path}")
                return False
            
            # 记录当前文件
            self._current_file = file_path
            
            # 使用SciPy读取WAV文件
            try:
                self.logger.info(f"尝试使用SciPy读取文件: {file_path}")
                sample_rate, audio_data = wavfile.read(file_path)
                self.logger.info(f"SciPy读取文件成功: {file_path}, 格式: {audio_data.dtype}")
                
                # 改进的格式转换逻辑
                if np.issubdtype(audio_data.dtype, np.floating):
                    # 浮点数据（通常是-1.0到1.0范围）转换为int16
                    self.logger.info("检测到浮点格式音频，使用改进的转换方法转为int16格式")
                    # 先确保浮点数据在-1.0到1.0范围内
                    max_val = np.max(np.abs(audio_data))
                    if max_val > 1.0:
                        self.logger.warning(f"浮点音频数据超出[-1.0, 1.0]范围，最大值: {max_val}，将进行归一化")
                        audio_data = audio_data / max_val
                    
                    # 应用一个小的缩放系数(0.95)防止最大值正好达到边界
                    safety_factor = 0.95
                    audio_data = np.int16(audio_data * 32767 * safety_factor)
                    
                elif audio_data.dtype == np.int32:
                    # 32位整数需要非常小心地缩放到16位
                    self.logger.info("检测到int32格式音频，使用改进的转换方法转为int16格式")
                    # 计算实际使用的位数（有些int32音频可能只使用了24位）
                    max_val = np.max(np.abs(audio_data))
                    bit_depth = int(np.ceil(np.log2(max_val + 1))) if max_val > 0 else 16
                    
                    self.logger.info(f"int32音频实际使用位数: {bit_depth}, 最大值: {max_val}")
                    
                    if bit_depth <= 16:
                        # 如果实际使用位数小于16，直接转换即可
                        audio_data = audio_data.astype(np.int16)
                    else:
                        # 使用更精确的缩放方法
                        shift_bits = bit_depth - 16
                        # 应用一个小的缩放系数(0.95)防止最大值正好达到边界
                        safety_factor = 0.95
                        audio_data = np.int16((audio_data / (2**shift_bits)) * safety_factor)
                    
                elif audio_data.dtype != np.int16:
                    # 其他类型转换为int16
                    self.logger.info(f"检测到非标准格式音频({audio_data.dtype})，将转换为int16格式")
                    # 先转换为float32进行规范化
                    audio_float = audio_data.astype(np.float32)
                    max_val = np.max(np.abs(audio_float))
                    if max_val > 0:
                        audio_float = audio_float / max_val * 32767 * 0.95
                    audio_data = audio_float.astype(np.int16)
                
                # 对所有类型的转换后进行最终检查，确保不超过int16范围
                max_possible = np.iinfo(np.int16).max
                if np.any(np.abs(audio_data) > max_possible):
                    self.logger.warning("转换后的音频数据超出int16范围，将进行裁剪")
                    audio_data = np.clip(audio_data, -max_possible, max_possible)
            
            except Exception as e:
                self.logger.error(f"SciPy读取文件失败: {str(e)}")
                return False
            
            # 保存音频数据
            self._audio_data = audio_data
            
            # 获取音频参数
            self._sample_rate = sample_rate
            if len(audio_data.shape) > 1:
                self._channels = audio_data.shape[1]
            else:
                self._channels = 1
            self._frames = len(audio_data)
            self._frame_position = 0
            
            # 配置音频流
            self.logger.info(f"音频参数: 通道数={self._channels}, 采样率={self._sample_rate}, 总帧数={self._frames}")
            
            # 检测是否为Mac系统并设置更大的缓冲区
            frames_per_buffer = 1024  # 默认缓冲区大小
            if platform.system() == 'Darwin':  # Mac系统
                frames_per_buffer = 4096  # Mac系统使用更大的缓冲区
                self.logger.info(f"检测到Mac系统，设置更大的缓冲区: {frames_per_buffer}")
            
            # 打开音频流
            try:
                self._stream = self.pa.open(
                    format=pyaudio.paInt16,
                    channels=self._channels,
                    rate=self._sample_rate,
                    output=True,
                    output_device_index=self._device_info['index'] if self._device_info else None,
                    stream_callback=self._audio_callback,
                    frames_per_buffer=frames_per_buffer  # 使用根据平台决定的缓冲区大小
                )
            except Exception as e:
                error_str = str(e)
                self.logger.error(f"打开音频流失败: {error_str}")
                
                # 检测特定的错误类型
                if "Device unavailable" in error_str:
                    device_name = self._current_device or "默认设备"
                    error_detail = f"设备不可用: {device_name}"
                    if self._device_info:
                        error_detail += f" (索引: {self._device_info.get('index', 'unknown')})"
                    self.logger.error(error_detail)
                    # 特别标记这是设备不可用错误
                    self.playback_error.emit("Device unavailable")
                else:
                    self.playback_error.emit(error_str)
                
                self._cleanup_stream()
                self._set_state(PlayerState.STOPPED)
                return False
            
            # 启动播放线程
            self._play_thread = threading.Thread(target=self._play_thread_func)
            self._play_thread.daemon = True
            self._play_thread.start()
            
            self.logger.info(f"开始播放: {file_path}")
            return True
            
        except Exception as e:
            self._cleanup_stream()
            self._set_state(PlayerState.STOPPED)
            self.logger.error(f"播放音频失败: {str(e)}")
            import traceback
            self.logger.error(f"异常堆栈: {traceback.format_exc()}")
            
            # 发送错误信号
            self.playback_error.emit(str(e))
            
            return False

    def stop(self) -> bool:
        """
        停止播放
        
        @return: 是否成功停止
        """
        if self._state == PlayerState.STOPPED:
            return True
        
        try:
            # 设置停止事件
            self._should_stop.set()
            
            # 等待线程结束
            with self._lock:
                if self._play_thread and self._play_thread.is_alive():
                    self._play_thread.join(0.5)  # 等待最多0.5秒
            
            # 清理临时文件
            self._clean_temp_file()
            
            self.logger.info("已停止播放")
            return True
        
        except Exception as e:
            self.logger.error(f"停止播放失败: {str(e)}")
            return False

    def pause(self) -> bool:
        """
        暂停播放
        
        @return: 是否成功暂停
        """
        self.logger.info(f"尝试暂停播放，当前状态: {self._state}, 暂停标志: {self._paused.is_set()}")
        
        if self._state != PlayerState.PLAYING:
            self.logger.warning(f"无法暂停：当前状态 {self._state} 不是播放状态")
            return False
        
        try:
            # 设置暂停事件
            self._paused.set()
            
            # 设置状态为暂停
            self._set_state(PlayerState.PAUSED)
            
            self.logger.info("已暂停播放")
            return True
        
        except Exception as e:
            self.logger.error(f"暂停播放失败: {str(e)}")
            import traceback
            self.logger.error(traceback.format_exc())
            return False

    def resume(self) -> bool:
        """
        恢复播放
        
        @return: 是否成功恢复
        """
        self.logger.info(f"尝试恢复播放，当前状态: {self._state}, 暂停标志: {self._paused.is_set()}")
        
        if self._state != PlayerState.PAUSED:
            self.logger.warning(f"无法恢复播放：当前状态 {self._state} 不是暂停状态")
            return False
        
        try:
            # 清除暂停事件
            self._paused.clear()
            
            # 更新状态为正在播放
            self._set_state(PlayerState.PLAYING)
            
            # 检查流是否还有效
            if self._stream and self._stream.is_active():
                self.logger.info("音频流处于活动状态，已恢复播放")
            else:
                self.logger.warning("音频流未处于活动状态，恢复可能失败")
                
            self.logger.info("已恢复播放")
            return True
        
        except Exception as e:
            self.logger.error(f"恢复播放失败: {str(e)}")
            import traceback
            self.logger.error(traceback.format_exc())
            return False

    def is_playing(self) -> bool:
        """
        是否正在播放
        
        @return: 是否正在播放
        """
        return self._state == PlayerState.PLAYING

    def is_paused(self) -> bool:
        """
        是否已暂停
        
        @return: 是否已暂停
        """
        return self._state == PlayerState.PAUSED

    def get_position(self) -> float:
        """
        获取当前播放位置（秒）
        
        @return: 当前播放位置
        """
        return self._position

    def get_duration(self) -> float:
        """
        获取音频总时长（秒）
        
        @return: 音频时长
        """
        return self._duration

    def seek(self, position: float) -> bool:
        """
        跳转到指定位置
        
        @param position: 目标位置（秒）
        @return: 是否成功跳转
        """
        # 暂时不支持跳转
        self.logger.warning("当前版本不支持跳转功能")
        return False

    def cleanup(self) -> None:
        """
        清理资源
        """
        # 停止播放
        self.stop()
        
        # 清理临时文件
        self._clean_temp_file()
        
        # 关闭PyAudio
        if self.pa:
            try:
                self.pa.terminate()
                self.pa = None
            except Exception as e:
                self.logger.error(f"关闭PyAudio时出错: {str(e)}")
        
        self.logger.info("PyAudio播放器资源已清理")

    def _set_state(self, state: PlayerState) -> None:
        """
        设置播放器状态
        
        @param state: 目标状态
        """
        if self._state != state:
            self._state = state
            self.state_changed.emit(str(state))

            if state == PlayerState.PLAYING:
                self.position_changed.emit(self._position)
                self.position_changed.emit(self._duration)
            elif state == PlayerState.PAUSED:
                self.position_changed.emit(self._position)
                self.position_changed.emit(self._duration)
            elif state == PlayerState.STOPPED:
                self.position_changed.emit(0.0)
                self.position_changed.emit(0.0)

            self.logger.debug(f"播放器状态已设置为: {state}")

    def get_state(self) -> str:
        """
        获取当前播放状态
        
        @return: 播放状态
        """
        return self._state.value 